AWS SAMのDynamoDBテーブル定義を別のCloudFormationテンプレートに分離してみた
AWS SAMでサーバーレスなアプリケーションを作っていると、1つのテンプレートファイル内にLambdaやDynamoDBの定義を書くと楽です。 しかし、あとから「DynamoDBの定義を分離したいな……」となることがたまにあります。例えば、CloudFormationのリソース上限200個が見えてきたり、そもそもデータストア層とアプリ層を同じテンプレートに書いて頻繁にデプロイ対象にしたくないなどです。
そんなとき、CloudFormationのインポート機能を使えばテンプレートファイルを分離できるのでは?と思ったので試してみました。
適当なAPIを作成する
アプリを初期化
sam init \ --runtime python3.7 \ --name CfnImportSampleApp \ --app-template hello-world
AWS SAMテンプレート
最初は、AWS SAMテンプレート内にDynamoDBテーブルの定義を書いています。
AWSTemplateFormatVersion: '2010-09-09' Transform: AWS::Serverless-2016-10-31 Description: CfnImportSampleApp Resources: HelloWorldFunction: Type: AWS::Serverless::Function Properties: CodeUri: hello_world/ Handler: app.lambda_handler Runtime: python3.7 Timeout: 5 Environment: Variables: TABLE_NAME: !Ref ParameterTable Policies: - arn:aws:iam::aws:policy/AmazonDynamoDBReadOnlyAccess Events: HelloWorld: Type: Api Properties: Path: /hello Method: get ParameterTable: Type: AWS::DynamoDB::Table Properties: TableName: CfnImportSampleTable AttributeDefinitions: - AttributeName: userId AttributeType: S KeySchema: - AttributeName: userId KeyType: HASH BillingMode: PAY_PER_REQUEST Outputs: HelloWorldApi: Value: !Sub "https://${ServerlessRestApi}.execute-api.${AWS::Region}.amazonaws.com/Prod/hello/"
Lambdaコード
import boto3 import json import os dynamodb = boto3.resource('dynamodb') table_name = os.environ['TABLE_NAME'] def lambda_handler(event, context): table = dynamodb.Table(table_name) res = table.get_item(Key={ 'userId': '1234' }) return { 'statusCode': 200, 'body': json.dumps({ 'message': 'hello world', 'data': res['Item'] }), }
デプロイ
sam build sam package \ --output-template-file packaged.yaml \ --s3-bucket cm-fujii.genki-deploy sam deploy \ --template-file packaged.yaml \ --stack-name Cfn-Import-Sample-Stack \ --capabilities CAPABILITY_NAMED_IAM \ --no-fail-on-empty-changeset
DynamoDBへデータ追加
適当に追加しました。
{ "todo": "xxx", "userId": "1234" }
APIを叩いて確認
実際にAPIを叩いてみると、下記の応答が返ってきます。バッチリですね。
$ curl https://xxx.execute-api.ap-northeast-1.amazonaws.com/Prod/hello/ {"message": "hello world", "data": {"userId": "1234", "todo": "xxx"}}
AWS SAMのDynamoDBを削除する
DynamoDBのDeletionPolicyをRetainにする
このままDynamoDBテーブルの定義を削除した場合、テーブル自体が削除されてしまいします。そのため、DeletionPolicy属性を設定することで、リソース(DynamoDBテーブル)を残しつつCloudFormationスタックからDynamoDBの定義を削除させます。
AWSTemplateFormatVersion: '2010-09-09' Transform: AWS::Serverless-2016-10-31 Description: CfnImportSampleApp Resources: HelloWorldFunction: Type: AWS::Serverless::Function Properties: CodeUri: hello_world/ Handler: app.lambda_handler Runtime: python3.7 Timeout: 5 Environment: Variables: TABLE_NAME: !Ref ParameterTable Policies: - arn:aws:iam::aws:policy/AmazonDynamoDBReadOnlyAccess Events: HelloWorld: Type: Api Properties: Path: /hello Method: get ParameterTable: Type: AWS::DynamoDB::Table DeletionPolicy: Retain Properties: TableName: CfnImportSampleTable AttributeDefinitions: - AttributeName: userId AttributeType: S KeySchema: - AttributeName: userId KeyType: HASH BillingMode: PAY_PER_REQUEST Outputs: HelloWorldApi: Value: !Sub "https://${ServerlessRestApi}.execute-api.${AWS::Region}.amazonaws.com/Prod/hello/"
DeletionPolicy: Retain
を付与したあと、再度デプロイを行います。これでAWS SAMテンプレートから削除する準備が整いました。
DynamoDBテーブルを削除する
AWS SAMテンプレートからDynamoDBテーブルの定義をまるっと削除します。Lambdaの環境変数部分も一度削除しておきます。 この作業中でもDynamoDBテーブルを参照したい場合は、環境変数部分にDynamoDBテーブル名を直接記載すると良いですね。
AWSTemplateFormatVersion: '2010-09-09' Transform: AWS::Serverless-2016-10-31 Description: CfnImportSampleApp Resources: HelloWorldFunction: Type: AWS::Serverless::Function Properties: CodeUri: hello_world/ Handler: app.lambda_handler Runtime: python3.7 Timeout: 5 Policies: - arn:aws:iam::aws:policy/AmazonDynamoDBReadOnlyAccess Events: HelloWorld: Type: Api Properties: Path: /hello Method: get Outputs: HelloWorldApi: Value: !Sub "https://${ServerlessRestApi}.execute-api.${AWS::Region}.amazonaws.com/Prod/hello/"
この状態でデプロイすると、CloudFormation管理リソースからDynamoDBが削除されました。
しかし、実際のDynamoDBテーブルは残り続けています。
CloudFormationで既存のDynamoDBテーブルをインポートする
CloudFormatioin用のテンプレートを作成する
datastore.yaml
というファイルを新規作成し、先ほどのDynamoDBテーブルをインポートさせます。なお、CloudFormationで既存リソースをインポートする場合はDeletionPolicy: Retain
が必須なので付与させています。
AWSTemplateFormatVersion: '2010-09-09' Resources: ParameterTable: Type: AWS::DynamoDB::Table DeletionPolicy: Retain Properties: TableName: CfnImportSampleTable AttributeDefinitions: - AttributeName: userId AttributeType: S KeySchema: - AttributeName: userId KeyType: HASH BillingMode: PAY_PER_REQUEST
CloudFormationで既存リソースをインポートする
CloudFormation画面で「既存のリソースを使用」を選択します。
先ほど作成したテンプレートファイル(datastore.yaml
)をアップロードして次に進みます。
識別子にインポートしたいDynamoDBテーブル名を入力して次に進みます。
- 識別子の値: CfnImportSampleTable
スタック名を入力して次に進みます。
- スタック名: Cfn-Import-Datastore-Sample-Stack
最終チェックを行い、既存リソースをインポートしましょう!
無事に完了しました!!
ここまでの作業で、DynamoDBテーブルの定義を分離できました。
AWS SAMで異なるテンプレートで定義したDynamoDBを参照する
異なるテンプレート(CloudFormationスタック)で定義したDynamoDBテーブルを参照する方法はいくつかあります。
- AWS SAMテンプレート内でDynamoDBテーブル名を固定文字列として持つ
- AWS SAMテンプレート内でクロススタック参照をする
今回はクロススタック参照を利用してみます。
Outputsを定義する
クロススタック参照を行うためには、まずOutputでExportする必要があります。
そのため、datastore.yaml
にOutputs
を追加します。
(最初から追加しようとしましたが、OutputsがあるテンプレートでCloudFormationの既存リソースはインポートできませんでした)
AWSTemplateFormatVersion: '2010-09-09' Resources: ParameterTable: Type: AWS::DynamoDB::Table DeletionPolicy: Retain Properties: TableName: CfnImportSampleTable AttributeDefinitions: - AttributeName: userId AttributeType: S KeySchema: - AttributeName: userId KeyType: HASH BillingMode: PAY_PER_REQUEST Outputs: ParameterTableName: Value: !Ref ParameterTable Export: Name: !Sub ${AWS::StackName}-ParameterTableName
デプロイします。
aws cloudformation deploy \ --template-file datastore.yaml \ --stack-name Cfn-Import-Datastore-Sample-Stack \ --capabilities CAPABILITY_NAMED_IAM \ --no-fail-on-empty-changeset
無事にOutputsできました。
AWS SAMで異なるスタックのDynamoDBテーブルを参照する(クロススタック参照)
Lambdaの環境変数でクロススタック参照を行うようにtemplate.yaml
を修正します。
AWSTemplateFormatVersion: '2010-09-09' Transform: AWS::Serverless-2016-10-31 Description: CfnImportSampleApp Resources: HelloWorldFunction: Type: AWS::Serverless::Function Properties: CodeUri: hello_world/ Handler: app.lambda_handler Runtime: python3.7 Timeout: 5 Environment: Variables: TABLE_NAME: Fn::ImportValue: Cfn-Import-Datastore-Sample-Stack-ParameterTableName Policies: - arn:aws:iam::aws:policy/AmazonDynamoDBReadOnlyAccess Events: HelloWorld: Type: Api Properties: Path: /hello Method: get Outputs: HelloWorldApi: Value: !Sub "https://${ServerlessRestApi}.execute-api.${AWS::Region}.amazonaws.com/Prod/hello/"
再デプロイを行いましょう。
動作確認
せっかくなのでDynamoDBの内容を書き換えました。
{ "todo": "finish!!!", "userId": "1234" }
この状態でAPIを叩くと、バッチリと返ってきました!!!
$ curl https://xxx.execute-api.ap-northeast-1.amazonaws.com/Prod/hello/ {"message": "hello world", "data": {"userId": "1234", "todo": "finish!!!"}}
さいごに
CloudFormationの既存リソースインポート機能を使って、DynamoDBテーブルの定義を別テンプレート(スタック)に分離してみました。 何らかの参考になれば幸いです。
参考
- 【アップデート】ついに来た!CloudFormationで手動で作成したリソースをStackにインポート可能になりました | Developers.IO
- CloudFormationがリソースのインポートに対応しました! | Developers.IO
- 既存リソースの CloudFormation 管理への取り込み - AWS CloudFormation
- スタックへの既存リソースのインポート - AWS CloudFormation
- インポートオペレーションをサポートするリソース - AWS CloudFormation
- CloudFormationのスタック間でリソースを参照する | Developers.IO